Skip to content

chore: dedupe retry/auth step helpers and small cleanups in http/pipeline/steps#196

Open
OmarAlJarrah wants to merge 4 commits into
mainfrom
chore/pipeline-steps-dedupe
Open

chore: dedupe retry/auth step helpers and small cleanups in http/pipeline/steps#196
OmarAlJarrah wants to merge 4 commits into
mainfrom
chore/pipeline-steps-dedupe

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Summary

Several behavior-preserving cleanups in sdk-core's http/pipeline/steps, where the sync/async step pairs had drifted into duplicated helper blocks. All changes are internal — no public signatures move, so apiCheck is unaffected — and land as separate commits.

Changes

Extract RetryPolicySupport (the structural one). DefaultRetryStep and DefaultAsyncRetryStep carried byte-for-byte copies of the entire stateless retry policy: options clamping, the RetrySettings backoff view, re-sendability gating, interrupt normalisation, predicate invocation, the caller delay override, and fixed/exponential backoff. Only the per-call loop machinery and terminal-failure paths genuinely differ. The policy now lives in one internal class RetryPolicySupport; both steps hold a support and delegate. Each step keeps what actually differs — the per-call should-retry checks (which read tryCount/suppressed), the distinct terminal paths, the protected open delay hooks, and closeQuietly with its deliberately different catch width (sync swallows IOException, async swallows Exception).

RetryPolicySupport takes (rawOptions, logger)not clock: none of the helpers that moved into it use the clock, and the only clock consumer (retryAfterFromHeaders) deliberately stays on each step. A clock field there would be dead and trip detekt.

Add HttpRetryOptions.withMaxRetries. HttpRetryOptions isn't a data class, so the negative-maxRetries clamp hand-rebuilt all eight fields through the constructor. A single internal copy helper lives on the type that owns the fields, and RetryPolicySupport.clampOptions collapses to one line. (A targeted helper beats converting to data class, which would push copy/componentN into the public API.)

Hoist offersBearerChallenge. Both bearer steps defined an identical private offersBearerChallenge + "bearer" constant. They move to one top-level internal function and constant in BearerTokenAuthStep.kt (mirroring the existing top-level defaultShouldRetryResponse); the async file drops its copy and its now-orphaned AuthChallengeParser import. evictRejectedToken stays per-step (it touches each step's own lock/cache).

Fold the AsyncAuthStep challenge carrier. handleChallenge ran the challenge future through a handle that only boxed (value, error) into a one-field HookOutcome, then a thenCompose that unboxed it to do the work. A single handle { … }.thenCompose { it } does the close/fail/dispatch decision directly, removing the carrier type and a composition stage. Behaviour is identical across all three completion paths (no-retry, retry, hook error).

Drop the throwaway MDC capture. DefaultAsyncInstrumentationStep initialised mdc with an MdcSnapshot.capture() that was overwritten on the first line inside the trace-context scope — one wasted snapshot per async request. lateinit var expresses the intent (assigned inside the scope, read only after it runs) without the wasted capture.

Verification

Locally on sdk-core: full test suite, compileKotlin (under allWarningsAsErrors), ktlint, detekt, and apiCheck all pass. I traced the AsyncAuthStep fold across normal/retry/exceptional challenge completions and confirmed the retry-policy helpers are byte-identical between the two stacks before extraction.

Closes #169

DefaultAsyncInstrumentationStep.processAsync initialised `mdc` with an
MdcSnapshot.capture() that is overwritten on the first line inside the
trace-context use{} block, so every async request paid for one MDC copy
that was immediately discarded. Use `lateinit var` instead: the real
capture still happens inside the scope (after trace.id / span.id are
pushed), the post-scope read is only reachable once the block has run, and
the wasted snapshot is gone.
handleChallenge ran the challenge future through a handle that only packed
its (value, error) into a one-field HookOutcome, then a thenCompose that
unpacked it to do the real work. Do the close / fail / dispatch decision
inside a single handle that returns the next CompletableFuture, and flatten
it with thenCompose { it }. This removes the carrier type and a whole
composition stage; the explicit null-check smart-casts the retry request
for the dispatch. Behaviour is identical across all three completion paths
(no retry, retry, hook error).
BearerTokenAuthStep and AsyncBearerTokenAuthStep each defined a private
offersBearerChallenge(response) and a private "bearer" scheme constant with
identical bodies. Hoist both to a single top-level internal function and
constant in BearerTokenAuthStep.kt (mirroring how defaultShouldRetryResponse
already lives top-level next to HttpRetryOptions) and call it from both
steps. The async file drops its copy and its now-orphaned AuthChallengeParser
import. evictRejectedToken stays per-step — it touches each step's own lock
and cached token.
DefaultRetryStep and DefaultAsyncRetryStep carried byte-for-byte copies of
the entire stateless retry policy: options clamping, the RetrySettings
backoff view, re-sendability gating, interrupt normalisation, predicate
invocation, the caller delay override, and fixed/exponential backoff. Only
the per-call loop machinery and terminal-failure paths genuinely differ
between the blocking and async stacks.

Pull the stateless policy into one internal RetryPolicySupport that clamps
the options and builds the backoff settings once and exposes the shared
helpers; both steps now hold a `support` and delegate. The retry
decision and backoff logic lives in one place, so a change to clamping,
backoff, or predicate-exception handling can no longer apply to only one
stack. Each step keeps what actually differs: the per-call should-retry
checks (they read tryCount / suppressed), the distinct terminal paths, the
protected-open delay hooks, and closeQuietly with its deliberately different
catch width (sync swallows IOException, async swallows Exception).

RetryPolicySupport takes only (rawOptions, logger): the clock is unused by
any helper that moved — retryAfterFromHeaders, the only clock consumer,
stays on each step.

Also add an internal HttpRetryOptions.withMaxRetries copy helper so the
negative-maxRetries clamp no longer hand-rebuilds all eight fields through
the constructor; RetryPolicySupport.clampOptions collapses to a single line.

All internal — no public signature changes, apiCheck unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

http/pipeline/steps: dedupe retry/auth step helpers and small cleanups

1 participant